iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 21
0
自我挑戰組

Android Architecture 及 Unit Test系列 第 21

[Day 21] Test:Part 3 Repository

  • 分享至 

  • xImage
  •  

今天打算完成 Repository 的測試,那就直接先開一個 TasksRepositoryTest 吧,一樣先初始化要測試的 TasksRepository ,不過今天有一點不一樣了。

TasksRepository 需要注入兩個 DataSource ,如果要建立這兩個 DataSource 又要再建立其他的依賴物,這時就可以使用 Mocking Framework 來幫助我們建立兩個假的 DataSource ,再把他們放到 TasksRepository 裡,從而解決我們的問題。

一般大家比較常用的 Mocking 工具是 Mockito ,但是在 Kotlin 裡使用 Mockito 會遇到一些問題,為了這個問題去年我在 JCConf 上詢問一位 JetBrains 的工程師時,他推薦我在寫 Kotlin 的測試時可以使用專門為 kotlin 打造的 MockK ,因此這次我特地使用 MockK 實作看看。

今天只會大概提到 MockK 在這個測試裡的使用情境,想要知道更詳細的教學可以參考官網或是其他人的心得

先來看看如何使用 MockK 建立兩個 DataSource :

@ExperimentalCoroutinesApi
class TasksRepositoryTest {

    @get:Rule
    var mainCoroutineRule = MainCoroutineRule()

    private lateinit var tasksLocalDataSource: TasksDataSource
    private lateinit var tasksRemoteDataSource: TasksDataSource

    private lateinit var tasksRepository: ITasksRepository

    @Before
    fun setup() {
        tasksLocalDataSource = mockk()
        tasksRemoteDataSource = mockk()

        tasksRepository = TasksRepository(
            tasksRemoteDataSource,
            tasksLocalDataSource,
            Dispatchers.Main
        )
    }
}

其實非常簡單,而且跟 Mockito 有一點相似,除此之外還有其他的 initial 方法,之後有機會也會提到。

還記得 TasksRepository 嗎?之前我寫了一個比較複雜的 getTasks 方法,如果想要為這個方法寫測試,要如何下手?

其實沒有十分困難,目標同樣是要測試 getTasks 方法的結果是否如預期所想的一樣,不需要在乎傳入的東西或是具體的執行流程。只是這次在 getTasks 裡有使用剛剛 mock 的兩個 DataSource ,這樣要如何使用這兩個假的 instance 呢?

事實上 Mock 可以假裝真的有 "調用" 這個方法,甚至還可以假裝這個方法被調用,而且還有回傳值。

這邊就可以利用 MockK 幫我們假裝 DataSource 內的方法有被調用或是有回傳

先來看看 getTasks 方法,追蹤一下 code 可以看到我們分別調用了 DataSource 裡的四個方法:

  • tasksRemoteDataSource.getTasks()
  • tasksLocalDataSource.deleteAllTasks()
  • tasksLocalDataSource.saveTask()
  • tasksLocalDataSource.getTasks()

這裏我們可以假裝這些方法有被調用或是有回傳值,這樣只要我們接住 getTasks 的結果並加以驗證,就可以完成這個測試了。

假如以 mock tasksRemoteDataSource.getTasks() 為例, MockK 提供了 every 方法,與 Mockito 的 when().thenReturn() 相似,目的都是指定呼叫目標方法 return 時一律都回傳某個值。

實作後會寫成這樣:

    @Test
    fun getTasksEmptyRepositoryThenReturnRemoteData() = runBlockingTest {
        val tasksRemote = listOf(
            Task("Title3", "Description3"),
            Task("Title4", "Description4")
        ).sortedBy { it.id }
        
        coEvery { tasksRemoteDataSource.getTasks() } returns Success(tasksRemote)
    }

這裏由於指定的方法使用了 Coroutines , MockK 又提供一個 coEvery 方法針對 Coroutines 處理;接著使用 returns 指定現在呼叫 tasksRemoteDataSource.getTasks() 一律回傳 Result.Success(tasksRemote)

另外像是 tasksLocalDataSource.deleteAllTasks 這種沒有回傳值的方法可以寫成以下:

    coEvery { tasksLocalDataSource.deleteAllTasks() } just Runs
    coEvery { tasksLocalDataSource.saveTask(any()) } just Runs

just Runs 表示遇到這個方法時可以直接執行 ,而 saveTask(any()) 則表示不在乎傳入的是什麼東西。

最後再進行驗證即可。

另外在測試一些沒有回傳值的方法時,有一種手法是驗證裡面的某些關鍵方法有沒有被調用, MockK 也提供 verify 方法解決,例如 tasksRepository.saveTask() 本身沒有回傳值,可以透過驗證 DataSource 的 saveTask()) 是否有被調用來當作測試結果:

    @Test
    fun saveTaskThenSaveToCacheLocalAndRemote() = runBlockingTest {
        // Given
        val newTask = Task("Title new", "Description new")
        coEvery { tasksLocalDataSource.saveTask(any()) } just Runs
        coEvery { tasksRemoteDataSource.saveTask(any()) } just Runs

        // When
        tasksRepository.saveTask(newTask)

        // Then
        coVerify { tasksLocalDataSource.saveTask(any()) }
        coVerify { tasksRemoteDataSource.saveTask(any()) }
    }

一樣由於有使用 Coroutines 所以改成用 MockK 的 coVerify 驗證。

今天大概到這邊,完整的 TasksRepositoryTest 可以看後面的連結


上一篇
[Day 20] Test:Part 2 DataSource
下一篇
[Day 22] Test:Part 4 UseCase and ViewModel
系列文
Android Architecture 及 Unit Test30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言